# -*- coding: utf-8 -*-

import sys, os
from twisted.python.log import err, msg

class UnresolvedDependency(Exception):
    pass

class CycledDependencyDetected(Exception):
    pass

class plugin:
    '''
    Класс, реализующий плагин
    Чтобы класс был доступен, его нужно упомянуть в __all__
    '''
    requires = [] # список зависимостей
    def __init__(self, bot):
        '''
        Конструктор.
        Вызывается при подключении плагина. Инициализирует плагин, производя все
        необходимые приготовления вроде регистрации обработчиков и инициализации соединений с СУБД.
        '''
        self.bot = bot # обратная связь с <s>Богом</s> ботом :)
    def unload(self):
        '''
        Вызывается при отключении плагина. Выполняет все действия,
        необходимые для корректной выгрузки плагина.
        '''
        self.bot = None
    def connected(self):
        ''' Стандартный обработчик события. Вызывается всякий раз при установлении соединения.
        Также он будет вызван при динамическом подключении плагина если бот онлайн.'''
        pass
    def disconnected(self):
        ''' Стандартный обработчик события. Вызывается всякий раз при разрыве соединения.
        Также он будет вызван перед динамической выгрузкой плагина.'''
        pass

class pluginManager:
    online = False
    def __init__(self, bot):
        """some initialization"""
        self.bot = bot
        self.cache = {}
        self.conflicts = {}
        self.plugins = {}
        self.__getitem__ = self.plugins.__getitem__
        self.has_key = self.plugins.has_key
        self.get = self.plugins.get
    def refresh(self):
        """обновляет кэшированный список плагинов, обходя директории, указанные
        в config.plugins_dirs. дополнительно перезагружает модули с плагинами"""
        msg("Refreshing plugins cache")
        self.cache = {}
        self.conflicts = {}
        for dir in self.bot.config.plugin_dirs:
            candidates = filter(lambda x: x.endswith('.py'), os.listdir(dir))
            sys.path.insert(0, dir)
            for f in candidates:
                try:
                    try:
                        exec "import %s as c"%f[:-3] in locals()
                        reload(c) # перезагрузим модуль. сохранять его мы никуда не будем, ибо как побочный эффект у нас все выражения import для данного плагина будут работать с новейшей версией модуля
                    except: pass
                    exec "from %s import __all__" % f[:-3] in locals()
                    plist = __all__
                except:
                    err("Can't import plugin list from %s (details below), skipping." % os.path.join(dir, f))
                    err()
                    plist = []
                if plist:
                    for pluginname in plist:
                        if pluginname in self.cache.keys()+self.conflicts.keys():
                            try:             del self.plugins[pluginname]
                            except KeyError: pass
                            
                            cfile = os.path.join(dir, f)
                            try:             self.conflicts[pluginname].append(cfile)
                            except KeyError: self.conflicts[pluginname] = [cfile]
                        else:
                            try:
                                exec "from %s import %s as plugin" % (f[:-3], pluginname) in globals()
                                self.cache[pluginname] = plugin
                            except:
                                err("Can't import plugin %s from file %s (details below)" % \
                                 (pluginname, os.path.join(dir, f)))
                                err()
            sys.path.pop(0)
    def restore(self):
        """restores plugin list from file, and loads them
        if file does not exists, initialize it with search()"""
        self.refresh()
        fn = os.path.join(self.bot.config.var_dir, 'pluginlist')
        if os.access(fn, os.R_OK):
           f = file(fn, 'r')
           plugins = f.readlines()
           f.close()
        else:
            # если списка нет, то грузим всё
            plugins = self.cache.keys()
        for plugin in plugins:
            if not plugin in self.plugins.keys():
                # если он ещё не загружен...
                try: self.load(plugin, True)
                except:
                    err("Failed to initialize plugin %s (details below), skipping" % plugin)
                    err()
    def save(self):
        """saves pluginlist to file"""
        fn = os.path.join(self.bot.config.var_dir, 'pluginlist')
        f = file(fn, 'w')
        f.write('\n'.join(self.plugins.keys()))
        f.close()
    def load(self, plugin, recursive=True):
        """loads plugin 'plugin' by name and all dependencies if recursive,
        loads only plugin 'plugin', and raise UnresolvedDependency
        if not recursive and some dependencies are not loaded"""
        if plugin in self.plugins.keys():
            raise NameError, '%s is already loaded!'%plugin
        if recursive:
            self.checkDependencies(plugin)
            loaded = self._recursive_load(plugin)
        else:
            p = self.cache[plugin]
            unresolved = set(p.requires) - set(self.plugins.keys())
            if unresolved: raise UnresolvedDependency, [unresolved]
            self.plugins[plugin] = p()
            loaded = [plugins]
        return loaded
    def _recursive_load(self, plugin):
        loaded = []
        if plugin not in self.plugins.keys():
            p = self.cache[plugin]
            map(lambda x: [loaded.append(y) for y in self._recursive_load(x)], p.requires)
            self.plugins[plugin] = p(self.bot)
            # после инициализации плагина симулируем подключение
            if self.online: self.plugins[plugin].connected()
            loaded.append(plugin)
        return loaded
    def checkDependencies(self, plugin, stack=[]):
        """рекурсивная функция, определяющая циклические зависимости в плагинах,
        на каждом шаге рекурсии имеем список плагинов (stack),
        которые прямо или косвенно требуют текущий плагин.
        Если в итоге окажется что плагин косвенно зависит от себя самого - выявлена зацикленность.

        Также она находит неразрешимые зависимости"""
        if plugin in stack: raise CycledDependencyDetected, stack
        stack.append(plugin)
        if not plugin in self.cache.keys(): raise UnresolvedDependency, stack
        map(lambda x: self.checkDependencies(x, stack), self.cache[plugin].requires)
        stack.pop()
    def unload(self, plugin, recursive=True):
        """unloads 'plugin' by name and all plugins, which depend on it, if recursive
        unloads 'plugin', but raise UnresolvedDependency, if smth depends on it if not recursive"""
        if plugin not in self.plugins.keys():
            raise NameError, '%s is not loaded!'%plugin
        unloaded = []
        if recursive:
            d = filter(lambda x: plugin in self.plugins[x].requires, self.plugins.keys())
            map(lambda x: [unloaded.append(y) for y in self.unload(x, True)], d)
            self._unload(plugin)
            unloaded.append(plugin)
        else:
            d = filter(lambda x: plugin in self.plugins[x].requires, self.plugins.keys())
            if d: raise UnresolvedDependency, d
            self._unload(plugin)
            unloaded.append(plugin)
        return unloaded
    def _unload(self, plugin):
        try:
            # симулируем дисконнект перед выгрузкой
            if self.online: self.plugins[plugin].disconnected()
            self.plugins[plugin].unload()
        except:
            err("failed to unload %s (details below)" % plugin)
            err()
        del self.plugins[plugin]
    def unloadAll(self):
        """unloads all of plugins()"""
        while self.plugins.keys():
            self.unload(self.plugins.keys()[0])
    def connected(self):
        self.online = True
        for x in self.plugins.values():
            try: x.connected()
            except: err()
    def disconnected(self):
        self.online = False
        for x in self.plugins.values():
            try: x.disconnected()
            except: err()

